---
title: "Explorer for Known and Predicted Kinase-Substrate Interactions"
editor: visual
author: "Chinmaya Joisa"
date: "09/08/2025"
toc: true
toc-depth: 5
toc-title: Table of Contents
highlight-style: pygments
format:
html:
embed-resources: true
code-fold: true
code-tools: true
execute:
echo: false
cache: false
message: false
warning: false
out-width: "100%"
fig-align: center
fig-dpi: 300
---
```{r setup, include=FALSE, cache = FALSE}
require("knitr")
knitr::opts_knit$set(root.dir = here::here())
set.seed(123)
```
```{r load_libraries, message=FALSE, warning=FALSE}
library(tidyverse)
library(tidygraph)
library(here)
library(org.Hs.eg.db)
library(clusterProfiler)
library(igraph)
library(ggraph)
library(scales)
library(readxl)
library(ggpubr)
library(jsonlite)
library(htmltools)
library(ggupset)
library(patchwork)
combined_pairs = read_csv(here("results/combined_kinase_substrate_pairs_2025.csv"))
```
```{r, echo=FALSE, results='asis'}
curated_sources <- c("PhosphoSitePlus","EPSD","iPTMNet","PhosphoELM","PhosphoNetworks")
edge_tbl <- combined_pairs %>%
mutate(evidence = if_else(Source %in% curated_sources, "Curated", "Predicted")) %>%
group_by(Kinase, Substrate) %>%
summarise(
evidence = if_else(any(evidence == "Curated"), "Curated", "Predicted"),
Likely_functional = any(Likely_functional),
SiteCount = n_distinct(Site),
Sites = paste(sort(unique(na.omit(Site))), collapse = "; "),
Reference_PMID = paste(sort(unique(Reference_PMID[!is.na(Reference_PMID) & Reference_PMID != ""])), collapse = "; "),
Sources = paste(sort(unique(Source)), collapse = "; "),
.groups = "drop"
) %>%
transmute(
source = Kinase,
target = Substrate,
evidence,
Likely_functional,
SiteCount,
Sites,
Reference_PMID,
edgeLabel = paste0(evidence, ifelse(Likely_functional, " · functional", "")),
SourceList = Sources
) %>%
mutate(Likely_functional = if_else(Likely_functional, "Likely functional", "Other/Unknown"))
node_tbl <- bind_rows(
combined_pairs %>% transmute(Gene = Kinase, Entrez = Kinase_entrez, role = "Kinase"),
combined_pairs %>% transmute(Gene = Substrate, Entrez = Substrate_entrez, role = "Substrate")
) %>%
mutate(Entrez = if_else(is.na(Entrez) | Entrez == "", NA_integer_, Entrez)) %>%
group_by(Gene) %>%
summarise(
Entrez = dplyr::first(na.omit(Entrez)),
role = paste(sort(unique(role)), collapse = ","),
.groups = "drop"
) %>%
arrange(Gene)
deg_tbl <- edge_tbl %>%
count(source, name = "outdeg") %>%
full_join(edge_tbl %>% count(target, name = "indeg"), by = c("source" = "target")) %>%
mutate(outdeg = coalesce(outdeg, 0L), indeg = coalesce(indeg, 0L)) %>%
transmute(Gene = source, outdeg, indeg) %>%
distinct()
#add uniprot ID to node_tbl (convert from entrezid) and dedupe many-to-one mappings
node_uniprot <- bitr(node_tbl$Entrez, fromType="ENTREZID", toType="UNIPROT", OrgDb="org.Hs.eg.db") %>%
distinct(ENTREZID, .keep_all = TRUE)
node_tbl <- node_tbl %>%
left_join(deg_tbl, by = "Gene") %>%
mutate(Entrez = as.character(Entrez)) %>%
left_join(node_uniprot, by = c("Entrez" = "ENTREZID")) %>%
group_by(Gene, Entrez, role) %>%
summarise(
outdeg = max(coalesce(outdeg, 0L), na.rm = TRUE),
indeg = max(coalesce(indeg, 0L), na.rm = TRUE),
UNIPROT = dplyr::first(na.omit(UNIPROT)),
.groups = "drop"
)
nodes_json <- node_tbl %>%
transmute(data = pmap(list(id = Gene, label = Gene, Entrez = Entrez, role = role,
indeg = indeg, outdeg = outdeg, deg = indeg + outdeg, uniprot = UNIPROT),
~ list(id = ..1, label = ..2, Entrez = ..3, role = ..4,
indeg = ..5, outdeg = ..6, deg = ..7, uniprot = ..8))) %>%
tidyr::unnest_wider(data)
edges_json <- edge_tbl %>%
transmute(data = pmap(list(source, target, evidence, Likely_functional, SiteCount, Sites, Reference_PMID, edgeLabel, SourceList),
~ list(source = ..1, target = ..2,
evidence = ..3,
Likely_functional = ..4,
SiteCount = ..5,
Sites = ..6,
Reference_PMID = ..7,
edgeLabel = ..8,
SourceList = ..9))) %>%
tidyr::unnest_wider(data)
payload <- list(nodes = nodes_json, edges = edges_json)
json_payload <- jsonlite::toJSON(payload, auto_unbox = TRUE)
json_kinases <- jsonlite::toJSON(sort(node_tbl$Gene[grepl("Kinase", node_tbl$role, fixed = TRUE)]), auto_unbox = TRUE)
json_subs <- jsonlite::toJSON(sort(node_tbl$Gene[grepl("Substrate", node_tbl$role, fixed = TRUE)]), auto_unbox = TRUE)
```
```{r}
data_tags <- tagList(
tags$script(id = "ks-data", type = "application/json", HTML(json_payload)),
tags$script(id = "kin-data", type = "application/json", HTML(json_kinases)),
tags$script(id = "sub-data", type = "application/json", HTML(json_subs))
)
viewer_tags <- tags$div(
`data-quarto-disable-processing` = "true",
tags$style(HTML("
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;600;700&family=Manrope:wght@400;600&display=swap');
:root {
--bg: #f5f1e6;
--panel: #f0ece2;
--panel-2: #e8e3d8;
--ink: #1f1b16;
--muted: #5b564c;
--accent: #2f2a24;
--accent-2: #4b4336;
--stroke: rgba(0,0,0,0.12);
--card-shadow: 0 10px 30px rgba(0,0,0,0.08);
}
body {font-family:'Manrope', 'Space Grotesk', sans-serif; background: repeating-linear-gradient(0deg, #f5f1e6 0px, #f5f1e6 18px, #f2ede3 18px, #f2ede3 36px); color: var(--ink);}
.page-shell{max-width:1200px;margin:0 auto;padding:24px 18px 42px;}
header#topbar{display:flex;align-items:center;justify-content:space-between;padding:14px 16px;margin-bottom:14px;border:1px solid var(--stroke);background:linear-gradient(120deg, rgba(0,0,0,0.02), rgba(0,0,0,0.01));border-radius:14px;box-shadow:var(--card-shadow);backdrop-filter: blur(2px);position:sticky;top:0;z-index:10}
#brand{display:flex;align-items:center;gap:10px;font-weight:700;font-size:18px;letter-spacing:0.4px;text-transform:uppercase;color:var(--ink)}
#brand .dot{width:10px;height:10px;border-radius:50%;background:var(--accent-2);box-shadow:none}
nav a{color:var(--muted);text-decoration:none;margin-left:16px;font-weight:600;font-size:13px;padding:6px 10px;border-radius:10px;border:1px solid transparent}
nav a:hover{color:var(--ink);border-color:var(--stroke);background:rgba(0,0,0,0.03)}
#hero{padding:22px 20px 18px;border:1px solid var(--stroke);border-radius:18px;background:linear-gradient(140deg, rgba(0,0,0,0.02), rgba(0,0,0,0.05));box-shadow:var(--card-shadow);margin-bottom:20px}
#hero h1{margin:0 0 10px;font-size:26px;letter-spacing:0.2px}
#hero p{margin:0;color:var(--muted)}
.stat-row{display:flex;gap:12px;flex-wrap:wrap;margin-top:14px}
.stat{flex:1 1 160px;border:1px solid var(--stroke);background:var(--panel);border-radius:14px;padding:10px 12px}
.stat .label{font-size:12px;color:var(--muted);text-transform:uppercase;letter-spacing:0.5px}
.stat .value{font-size:20px;font-weight:700;color:var(--ink)}
.section-head{display:flex;align-items:center;justify-content:space-between;margin:18px 2px 10px}
.section-head h3{margin:0;font-size:18px}
.section-head .pill{font-size:12px;padding:4px 8px;border-radius:999px;border:1px solid var(--stroke);color:var(--muted)}
.panel{border:1px solid var(--stroke);background:var(--panel);border-radius:16px;padding:14px 14px 10px;box-shadow:var(--card-shadow)}
#controls{display:flex;flex-direction:column;gap:10px}
.control-row{display:flex;gap:10px;flex-wrap:wrap;align-items:center}
.control-row .field-label{font-weight:600;color:var(--muted);margin-right:4px}
#exportbar{display:flex;gap:10px;flex-wrap:wrap;margin:10px 0 10px 0}
#legend{display:flex;gap:16px;flex-wrap:wrap;align-items:center;margin:4px 0 8px 0;font-size:13px;color:var(--muted)}
#legend .chip{display:inline-flex;align-items:center;gap:6px;padding:4px 6px;border-radius:10px;border:1px solid var(--stroke);background:var(--panel-2)}
#legend .line{width:32px;height:0;border-top:3px solid #999}
#legend .line.known{border-color:#1f1b16}
#legend .line.novel{border-color:#5f564c}
#legend .dot{width:14px;height:14px;border-radius:50%;display:inline-block;border:1px solid #444}
#legend .kin{background:#f4efe4}
#legend .sub{background:#e9e2d6}
.legend-note{color:var(--muted)}
#cywrap{margin-top:10px}
#cy {width:100%;height:720px;border:1px solid var(--stroke);border-radius:12px;background:#f7f2e8}
.label {font-weight:600;color:var(--muted)}
.badge {display:inline-block;padding:2px 8px;border-radius:999px;margin-left:6px;border:1px solid var(--stroke);color:var(--muted);background:var(--panel-2)}
.kbadge{background:#f4efe4;border-color:#3b352c;color:#1f1b16}
.sbadge{background:#e9e2d6;border-color:#4a4339;color:#1f1b16}
.tablewrap{margin-top:12px}
.filters {display:flex;gap:8px;flex-wrap:wrap;margin:8px 0}
.filters input {padding:8px 10px;border:1px solid var(--stroke);border-radius:10px;font-size:13px;background:var(--panel);color:var(--ink)}
.filters input:focus{outline:1px solid var(--accent)}
.tablebox {max-height:400px; overflow:auto; border:1px solid var(--stroke); border-radius:12px;background:var(--panel-2)}
.tbl {border-collapse:collapse;width:100%}
.tbl th, .tbl td{border-bottom:1px solid var(--stroke);padding:8px 10px;font-size:13px;text-align:left;white-space:nowrap;color:var(--ink)}
.tbl th{background:rgba(0,0,0,0.02); position: sticky; top: 0; z-index: 1; color:var(--muted)}
.btn{padding:8px 12px;border:1px solid var(--stroke);border-radius:10px;background:linear-gradient(120deg, rgba(0,0,0,0.02), rgba(0,0,0,0.04));cursor:pointer;color:var(--ink);font-weight:600}
.btn:hover{background:linear-gradient(120deg, rgba(0,0,0,0.05), rgba(0,0,0,0.07))}
.btn.primary{border-color:var(--accent);box-shadow:none}
.tip{margin:10px 0 4px;border:1px dashed var(--stroke);border-radius:10px;padding:8px 10px;font-size:13px;color:var(--muted);background:rgba(0,0,0,0.02)}
select, input[type='checkbox'], input[list]{background:var(--panel-2);color:var(--ink);border:1px solid var(--stroke);border-radius:8px;padding:6px 8px}
select{padding:8px 10px}
input[list]{min-width:220px}
@media (max-width: 768px){#controls{flex-direction:column;align-items:flex-start} #cy{height:520px}}
")),
tags$div(
class="page-shell",
tags$header(id="topbar",
tags$div(id="brand", tags$span(class="dot"), "KinaseCanvas Explorer"),
tags$nav(
tags$a(href="#network", "Network"),
tags$a(href="#table", "Edges"),
tags$a(href="https://kinet.kinametrix.com/#section-proteins", target="_blank", "Reference")
)
),
tags$section(id="hero",
tags$h1("Curated kinase–substrate network, ready to explore"),
tags$p("Seed the graph with a kinase and/or substrate, filter on functional evidence, and export images or tables for publication."),
tags$div(class="stat-row",
tags$div(class="stat", tags$div(class="label", "Kinases"), tags$div(id="statKinases", class="value", "—")),
tags$div(class="stat", tags$div(class="label", "Substrates"), tags$div(id="statSubstrates", class="value", "—")),
tags$div(class="stat", tags$div(class="label", "Interactions"), tags$div(id="statEdges", class="value", "—")),
tags$div(class="stat", tags$div(class="label", "Curated share"), tags$div(id="statCurated", class="value", "—"))
)
),
tags$section(id="network",
class="section",
tags$div(class="section-head", tags$h3("Network"), tags$span(class="pill", "Choose seeds, then explore")),
tags$div(class="panel",
tags$div(
id = "controls",
tags$div(class="control-row",
tags$label(class="label", "Kinase:", `for`="kinSel"),
tags$input(id="kinSel", list="kinList", placeholder="Type a kinase…", style="min-width:240px"),
tags$datalist(id="kinList"),
tags$span(class="badge kbadge", "Kinase seed")
),
tags$div(class="control-row",
tags$label(class="label", "Substrate:", `for`="subSel"),
tags$input(id="subSel", list="subList", placeholder="Type a substrate… (optional)", style="min-width:240px"),
tags$datalist(id="subList"),
tags$span(class="badge sbadge", "Substrate seed")
),
tags$div(class="control-row",
tags$label(class="label", "Layout:", `for`="layoutSel"),
tags$select(
id="layoutSel",
tags$option(value="fcose","fCoSE"),
tags$option(value="cose","COSE"),
tags$option(value="cose-bilkent","CoSE-Bilkent"),
tags$option(value="concentric","Concentric"),
tags$option(value="breadthfirst","Breadthfirst")
),
tags$label(class="label", "Label size:", `for`="labelScale", style="margin-left:12px"),
tags$input(type="range", id="labelScale", min="50", max="180", value="100", step="10", style="width:140px"),
tags$span(id="labelScaleVal", class="badge", "100%")
),
tags$div(class="control-row",
tags$label(tags$input(type="checkbox", id="edgeLabelsChk"), " Edge labels"),
tags$label(tags$input(type="checkbox", id="arrowsChk", checked=NA), " Arrows"),
tags$label(tags$input(type="checkbox", id="onlyFunctionalChk"), " Functional sites only"),
tags$label(tags$input(type="checkbox", id="curatedOnlyChk"), " Curated pairs only")
)
),
tags$div(id="legend",
tags$span(class="chip", tags$span(class="dot kin"), "Kinase"),
tags$span(class="chip", tags$span(class="dot sub"), "Substrate"),
tags$span(class="chip", tags$span(class="line known"), "Curated"),
tags$span(class="chip", tags$span(class="line novel"), "Predicted"),
tags$span(class="legend-note", "Dotted = not flagged functional; Solid = likely functional; Size of edge does not indicate strength")
),
tags$div(class="tip", "Click and hold to move around nodes. Scroll to zoom. Click nodes to open UniProt page."),
tags$div(id="exportbar",
tags$button(id="btnPng", class="btn primary", "Export PNG"),
tags$button(id="btnPdf", class="btn", "Export PDF"),
tags$button(id="btnCsvEdges", class="btn", "Export Edge CSV")
),
tags$div(id="cywrap", tags$div(id="cy"))
)
),
tags$section(id="table", class="section",
tags$div(class="section-head", tags$h3("Edges in View"), tags$span(class="pill", "Filter columns")),
tags$div(class="panel",
tags$div(id="edgesFilters", class="filters"),
tags$div(id="edgesTable", class="tablebox")
)
)
),
tags$script(src="https://unpkg.com/cytoscape@3.26.0/dist/cytoscape.min.js"),
tags$script(src="https://unpkg.com/layout-base/layout-base.js"),
tags$script(src="https://unpkg.com/cose-base/cose-base.js"),
tags$script(src="https://unpkg.com/cytoscape-fcose@2.2.0/cytoscape-fcose.js"),
tags$script(src="https://unpkg.com/cytoscape-cose-bilkent@4.1.0/cytoscape-cose-bilkent.js"),
tags$script(src="https://cdn.jsdelivr.net/npm/jspdf@2.5.1/dist/jspdf.umd.min.js"),
tags$script(HTML("
(function(){
const data = JSON.parse(document.getElementById('ks-data').textContent);
const kinases = JSON.parse(document.getElementById('kin-data').textContent);
const substrates = JSON.parse(document.getElementById('sub-data').textContent);
let currentSeeds = [];
const maxDeg = data.nodes.reduce((m,n) => Math.max(m, (n.indeg || 0) + (n.outdeg || 0)), 0);
const stats = {
kinases: kinases.length,
substrates: substrates.length,
edges: data.edges.length,
curated: data.edges.filter(e => e.evidence === 'Known (curated)').length
};
const setStat = (id, text) => { const el = document.getElementById(id); if(el) el.textContent = text; };
setStat('statKinases', stats.kinases.toLocaleString());
setStat('statSubstrates', stats.substrates.toLocaleString());
setStat('statEdges', stats.edges.toLocaleString());
setStat('statCurated', stats.edges ? Math.round((stats.curated / stats.edges) * 100).toString() + '% curated' : '—');
const kinDL = document.getElementById('kinList');
kinases.forEach(g => { const opt = document.createElement('option'); opt.value = g; kinDL.appendChild(opt); });
const subDL = document.getElementById('subList');
substrates.forEach(g => { const opt = document.createElement('option'); opt.value = g; subDL.appendChild(opt); });
// default seeds so something renders immediately
const kinInput = document.getElementById('kinSel');
const subInput = document.getElementById('subSel');
if(kinases.length){ kinInput.value = kinases[0]; }
if(substrates.length){ subInput.placeholder = 'Type a substrate… (optional)'; }
const labelScale = document.getElementById('labelScale');
const labelScaleEl = document.getElementById('labelScaleVal');
const baseFont = 9;
const basePad = 5;
const cy = cytoscape({
container: document.getElementById('cy'),
elements: [],
style: [
{ selector: 'node',
style: {
'shape': 'round-rectangle',
'background-color': '#f6f1e7',
'label': 'data(label)',
'font-size': 10,
'color': '#1f1b16',
'text-opacity': 1,
'text-wrap': 'wrap',
'text-max-width': '100px',
'min-zoomed-font-size': 6,
'text-margin-y': -2,
'text-halign': 'center',
'text-valign': 'center',
'border-color': '#2f281e',
'border-width': 0.9,
'width': 'label',
'height': 'label',
'padding': '4px'
}
},
{ selector: 'node[role *= \"Substrate\"]:not([role *= \"Kinase\"])',
style: { 'background-color': '#e1d7c7', 'border-color': '#6a5c4a' } },
{ selector: 'node[role *= \"Kinase\"]',
style: { 'background-color': '#fbf6ec', 'border-color': '#2a241b' } },
{ selector: 'node.qk', style: { 'border-width': 2.5 } },
{ selector: 'node.qs', style: { 'border-width': 2.5 } },
{ selector: 'edge',
style: {
'width': 1.6,
'curve-style': 'bezier',
'line-color': '#4f4a42',
'target-arrow-color': '#4f4a42',
'target-arrow-shape': 'triangle',
'label': 'data(edgeLabel)',
'font-size': 9,
'text-rotation': 'autorotate',
'text-background-color': '#ffffff',
'text-background-opacity': 0.85,
'text-background-padding': 2,
'text-margin-y': -2
}
},
{ selector: 'edge[evidence = \"Curated\"]',
style: { 'line-color': '#16110c', 'target-arrow-color': '#16110c' } },
{ selector: 'edge[evidence = \"Predicted\"]',
style: { 'line-color': '#7b6d5d', 'target-arrow-color': '#7b6d5d' } },
{ selector: 'edge[Likely_functional = \"Likely functional\"]',
style: { 'line-style': 'solid' } },
{ selector: 'edge[Likely_functional = \"Other/Unknown\"]',
style: { 'line-style': 'dotted' } }
],
layout: { name: 'fcose' }
});
cy.on('tap', 'node', evt => {
const up = (evt.target && evt.target.data) ? evt.target.data('uniprot') : null;
if(!up) return;
const url = 'https://www.uniprot.org/uniprotkb/' + encodeURIComponent(up);
window.open(url, '_blank', 'noopener');
});
function setEdgeLabels(show){
cy.style().selector('edge').style('label', show ? 'data(edgeLabel)' : '').update();
}
function setArrows(show){
cy.style().selector('edge').style('target-arrow-shape', show ? 'triangle' : 'none').update();
}
function applyNodeSizing(){
const scale = Number(labelScale.value || 100) / 100;
if(labelScaleEl){ labelScaleEl.textContent = Math.round(scale * 100) + '%'; }
cy.batch(() => {
cy.nodes().forEach(n => {
const fontSize = Math.max(7, Math.min(14, baseFont * scale));
const pad = Math.max(3, Math.min(12, basePad * scale));
n.style({ 'font-size': fontSize, 'padding': pad });
});
});
}
function isFunctionalEdge(e){
const v = (e.data('Likely_functional') ?? '').trim();
return v === 'Likely functional';
}
function isCuratedEdge(e){
const v = (e.data('evidence') ?? '').trim();
return v === 'Known (curated)';
}
function applyFilters(doLayout = true){
const onlyFunctional = document.getElementById('onlyFunctionalChk').checked;
const curatedOnly = document.getElementById('curatedOnlyChk')?.checked;
const seedSet = new Set(currentSeeds || []);
cy.batch(() => {
cy.edges().forEach(e => {
const keep = (!curatedOnly || isCuratedEdge(e)) && (!onlyFunctional || isFunctionalEdge(e));
e.style('display', keep ? 'element' : 'none');
});
cy.nodes().forEach(n => {
const hasVisibleEdge = n.connectedEdges(':visible').length > 0;
const isSeed = seedSet.has(n.id());
n.style('display', (hasVisibleEdge || isSeed) ? 'element' : 'none');
});
});
if(doLayout){ relayout(document.getElementById('layoutSel').value); }
}
function refreshEdgesTable(){
const rows = cy.edges(':visible').map(e => [
e.data('source'),
e.data('target'),
e.data('evidence'),
e.data('Likely_functional'),
e.data('SiteCount'),
e.data('Sites'),
e.data('SourceList') ?? '',
e.data('Reference_PMID')
]);
renderFilterableTable('edgesFilters','edgesTable',
['From','To','Evidence','Functional Site?','#Sites','Sites','Sources','Reference_PMID'], rows);
}
function renderFilterableTable(filtersId, tableBoxId, headers, rows){
const fwrap = document.getElementById(filtersId);
const box = document.getElementById(tableBoxId);
fwrap.innerHTML = '';
box.innerHTML = '';
const filters = headers.map(h => {
const inp = document.createElement('input');
inp.type = 'text';
inp.placeholder = 'Filter ' + h;
fwrap.appendChild(inp);
return inp;
});
const table = document.createElement('table');
table.className = 'tbl';
const thead = document.createElement('thead');
const trh = document.createElement('tr');
headers.forEach(h => { const th = document.createElement('th'); th.textContent = h; trh.appendChild(th); });
thead.appendChild(trh); table.appendChild(thead);
const tbody = document.createElement('tbody'); table.appendChild(tbody); box.appendChild(table);
function passes(row, query){
for(let i=0;i<query.length;i++){
const q = query[i];
if(q && !String(row[i] ?? '').toLowerCase().includes(q)) return false;
}
return true;
}
function draw(){
const q = filters.map(x => x.value.trim().toLowerCase());
tbody.innerHTML = '';
rows.forEach(r => {
if(!passes(r, q)) return;
const tr = document.createElement('tr');
r.forEach(v => { const td = document.createElement('td'); td.textContent = (v ?? '').toString(); tr.appendChild(td); });
tbody.appendChild(tr);
});
}
filters.forEach(inp => inp.addEventListener('input', draw));
draw();
}
function incidentSubgraph(seeds){
const nodeIndex = new Map(data.nodes.map(n => [n.id, n]));
const validSeeds = seeds.filter(s => nodeIndex.has(s));
if(!validSeeds.length){ return {nodes:[], edges:[]}; }
const edges = data.edges.filter(e => validSeeds.includes(e.source) || validSeeds.includes(e.target));
if(!edges.length){ return {nodes:[], edges:[]}; }
const nset = new Set(); edges.forEach(e => { nset.add(e.source); nset.add(e.target); });
const nodes = Array.from(nset).map(id => ({
data: nodeIndex.get(id),
classes: (validSeeds.includes(id) ? (seeds[0]===id ? 'qk' : (seeds[1]===id ? 'qs' : '')) : '')
}));
return {nodes: nodes.map(n => ({ data: n.data, classes: n.classes })), edges: edges.map(e => ({ data: e }))};
}
function relayout(name){
const opts = (name==='concentric') ? { name, minNodeSpacing: 20 } :
(name==='breadthfirst') ? { name, directed: true, padding: 10 } :
(name==='fcose') ? {
name,
quality: 'default',
randomize: true,
animate: false,
animationDuration: 1000,
fit: true,
padding: 30,
nodeDimensionsIncludeLabels: false,
uniformNodeDimensions: false,
packComponents: false,
tile: true,
samplingType: true,
sampleSize: 25,
nodeSeparation: 90,
nodeRepulsion: node => 4500,
idealEdgeLength: edge => 80,
edgeElasticity: edge => 0.45,
nestingFactor: 0.1,
numIter: 2500,
gravity: 0.25,
gravityRangeCompound: 1.5,
gravityCompound: 1.0,
gravityRange: 3.8,
initialEnergyOnIncremental: 0.3
} :
(name==='cose-bilkent') ? {
name,
animate: false,
quality: 'draft',
nodeRepulsion: 30000,
idealEdgeLength: 140,
gravity: 0.8,
tile: true,
nodeDimensionsIncludeLabels: true,
padding: 30
} :
{ name: 'cose', animate: false };
cy.layout(opts).run();
}
function render(){
const kin = document.getElementById('kinSel').value.trim();
const sub = document.getElementById('subSel').value.trim();
const seeds = [kin, sub].filter(s => s && s.length);
if(!seeds.length) return;
currentSeeds = seeds;
const elems = incidentSubgraph(seeds);
cy.elements().remove();
cy.add(elems.nodes);
cy.add(elems.edges);
applyNodeSizing();
applyFilters(true);
refreshEdgesTable();
}
const triggerRender = () => render();
const triggerOnEnter = el => el.addEventListener('keyup', e => { if(e.key === 'Enter') render(); });
kinInput.addEventListener('change', triggerRender);
subInput.addEventListener('change', triggerRender);
triggerOnEnter(kinInput);
triggerOnEnter(subInput);
document.getElementById('layoutSel').addEventListener('change', () => render());
document.getElementById('edgeLabelsChk').addEventListener('change', e => setEdgeLabels(e.target.checked));
document.getElementById('arrowsChk').addEventListener('change', e => setArrows(e.target.checked));
document.getElementById('onlyFunctionalChk').addEventListener('change', () => {
applyFilters(true);
refreshEdgesTable();
});
document.getElementById('curatedOnlyChk').addEventListener('change', () => {
applyFilters(true);
refreshEdgesTable();
});
labelScale.addEventListener('input', applyNodeSizing);
function download(filename, blob){
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = filename;
document.body.appendChild(a); a.click(); a.remove();
setTimeout(() => URL.revokeObjectURL(a.href), 5000);
}
function csvBlob(rows, header){
const esc = v => '\"' + String(v ?? '').replaceAll('\"','\"\"') + '\"';
const lines = [header.map(esc).join(',')].concat(rows.map(r => r.map(esc).join(',')));
const newline = String.fromCharCode(10);
const csv = lines.join(newline);
return new Blob([csv], {type:'text/csv'});
}
document.getElementById('btnPng').addEventListener('click', () => {
const uri = cy.png({full:true, scale:2});
fetch(uri).then(r => r.blob()).then(b => download('kinome_network.png', b));
});
document.getElementById('btnPdf').addEventListener('click', () => {
const uri = cy.png({full:true, scale:2});
fetch(uri).then(r => r.blob()).then(b => {
const reader = new FileReader();
reader.onload = function(){
const { jsPDF } = window.jspdf;
const pdf = new jsPDF({orientation:'landscape', unit:'pt', format:'a4'});
const img = reader.result;
const pageW = pdf.internal.pageSize.getWidth();
const pageH = pdf.internal.pageSize.getHeight();
pdf.addImage(img, 'PNG', 20, 20, pageW-40, pageH-40);
pdf.save('kinome_network.pdf');
};
reader.readAsDataURL(b);
});
});
document.getElementById('btnCsvEdges').addEventListener('click', () => {
const rows = cy.edges(':visible').map(e => [
e.data('source'),
e.data('target'),
e.data('evidence'),
e.data('Likely_functional'),
e.data('SiteCount'),
e.data('Sites'),
e.data('SourceList') ?? '',
e.data('Reference_PMID')
]);
const blob = csvBlob(rows, ['From','To','Evidence','Functional Site?','#Sites','Sites','Sources','Reference_PMID']);
download('kinome_edges.csv', blob);
});
setEdgeLabels(false);
setArrows(true);
applyNodeSizing();
render();
})();
"))
)
browsable(tagList(data_tags, viewer_tags))
```